/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shindig.gadgets.rewrite;

import org.apache.commons.lang.StringUtils;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.gadgets.Gadget;
import org.apache.shindig.gadgets.uri.ConcatUriManager;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.google.common.collect.Lists;

import java.util.Iterator;
import java.util.List;

public class ConcatVisitor implements DomWalker.Visitor {
  public static class Js extends ConcatVisitor {
    public Js(ContentRewriterFeature.Config config,
              ConcatUriManager uriManager) {
      super(config, uriManager, ConcatUriManager.Type.JS);
    }
  }
  
  public static class Css extends ConcatVisitor {
    public Css(ContentRewriterFeature.Config config,
               ConcatUriManager uriManager) {
      super(config, uriManager, ConcatUriManager.Type.CSS);
    }
  }
  
  private final ConcatUriManager uriManager;
  private final ConcatUriManager.Type type;
  private final ContentRewriterFeature.Config config;
  private final boolean split;
  
  private ConcatVisitor(ContentRewriterFeature.Config config,
      ConcatUriManager uriManager, ConcatUriManager.Type type) {
    this.uriManager = uriManager;
    this.type = type;
    this.config = config;
    this.split = (type == ConcatUriManager.Type.JS && config.isSplitJsEnabled());
  }

  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
    // Reserve JS nodes; always if there's an adjacent rewritable JS node and also when
    // directed to support split-resource concatenation
    if (node.getNodeType() != Node.ELEMENT_NODE ||
        !node.getNodeName().equalsIgnoreCase(type.getTagName())) {
      return VisitStatus.BYPASS;
    }
    
    Element element = (Element)node;
    if (isRewritableExternData(element)) {
      if (split ||
          isRewritableExternData(getSibling(element, true)) ||
          isRewritableExternData(getSibling(element, false))) {
        return VisitStatus.RESERVE_NODE;
      }
    }
    
    return VisitStatus.BYPASS;
  }
  
  public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
    // Collate Elements into batches to be concatenated. 
    List<List<Element>> concatBatches = Lists.newLinkedList();
    List<Element> curBatch = Lists.newLinkedList();
    Iterator<Node> nodeIter = nodes.iterator();
    Element cur = (Element)nodeIter.next();
    curBatch.add(cur);
    while (nodeIter.hasNext()) {
      Element next = (Element)nodeIter.next();
      if (!split && cur != getSibling(next, true)) {
        // Break off current batch and add to list of all.
        concatBatches.add(curBatch);
        curBatch = Lists.newLinkedList();
      }
      curBatch.add(next);
      cur = next;
    }
    
    // Add leftovers.
    concatBatches.add(curBatch);
    
    // Prepare batches of Uris to send to generate concat Uris
    List<List<Uri>> uriBatches = Lists.newLinkedList();
    Iterator<List<Element>> batchesIter = concatBatches.iterator();
    while (batchesIter.hasNext()) {
      List<Element> batch = batchesIter.next();
      List<Uri> uris = Lists.newLinkedList();
      if (batch.isEmpty() || !getUris(type, batch, uris)) {
        batchesIter.remove();
        continue;
      }
      uriBatches.add(uris);
    }
    
    if (uriBatches.isEmpty()) {
      return false;
    }
    
    // Generate the ConcatUris, then correlate with original elements.
    List<ConcatUriManager.ConcatData> concatUris =
        uriManager.make(
          ConcatUriManager.ConcatUri.fromList(gadget, uriBatches, type), !split);
    
    Iterator<List<Element>> elemBatchIt = concatBatches.iterator();
    Iterator<List<Uri>> uriBatchIt = uriBatches.iterator();
    for (ConcatUriManager.ConcatData concatUri : concatUris) {
      List<Element> sourceBatch = elemBatchIt.next();
      List<Uri> sourceUris = uriBatchIt.next();
      
      // Regardless what happens, inject a copy of the first node,
      // with new (concat) URI, immediately ahead of the first elem.
      Element firstElem = sourceBatch.get(0);
      Element elemConcat = (Element)firstElem.cloneNode(true);
      elemConcat.setAttribute(type.getSrcAttrib(), concatUri.getUri().toString());
      firstElem.getParentNode().insertBefore(elemConcat, firstElem);
      
      // Now for all Elements, either A) remove them or B) replace each
      // with a <script> node with snippet of code configuring/evaluating
      // the resultant inserted code. This is useful for split-JS in particular,
      // and might also be used in spriting later.
      Iterator<Uri> uriIt = sourceUris.iterator();
      for (Element elem : sourceBatch) {
        Uri elemOrigUri = uriIt.next();
        String snippet = concatUri.getSnippet(elemOrigUri);
        if (!StringUtils.isEmpty(snippet)) {
          Node scriptNode = elem.getOwnerDocument().createElement("script");
          scriptNode.setTextContent(snippet);
          elem.getParentNode().insertBefore(scriptNode, elem);
        }
        elem.getParentNode().removeChild(elem);
      }
    }
    
    return true;
  }
  
  private boolean isRewritableExternData(Element elem) {
    String uriStr = elem != null ? elem.getAttribute(type.getSrcAttrib()) : null;
    if (StringUtils.isEmpty(uriStr) ||
        !config.shouldRewriteURL(uriStr)) {
      return false;
    }
    if (type == ConcatUriManager.Type.CSS) {
      // rel="stylesheet" and type="css" also required.
      return ("stylesheet".equalsIgnoreCase(elem.getAttribute("rel")) ||
              elem.getAttribute("type").contains("css"));
    }
    return true;
  }
  
  private Element getSibling(Element root, boolean isPrev) {
    Node cur = root;
    while ((cur = getNext(cur, isPrev)) != null) {
      if (cur.getNodeType() == Node.TEXT_NODE && StringUtils.isEmpty(cur.getTextContent())) {
        continue;
      }
      break;
    }
    if (cur != null && cur.getNodeType() == Node.ELEMENT_NODE) {
      return (Element)cur;
    }
    return null;
  }
  
  private Node getNext(Node node, boolean isPrev) {
    return isPrev ? node.getPreviousSibling() : node.getNextSibling();
  }
  
  private boolean getUris(ConcatUriManager.Type type, List<Element> elems, List<Uri> uris) {
    for (Element elem : elems) {
      String uriStr = elem.getAttribute(type.getSrcAttrib());
      try {
        uris.add(Uri.parse(uriStr));
      } catch (Uri.UriException e) {
        // Invalid formatted Uri, batch failed.
        return false;
      }
    }
    return true;
  }

}
